# -*- coding: utf-8 -*-

# Copyright (c) Huoty, All rights reserved
# Author: Huoty <sudohuoty@163.com>
# CreateTime: 2017-06-26 19:38:56 Monday

"""异常重试装饰器"""

import sys
import time
import random
import pprint
import logging
import functools
from copy import copy


if sys.version_info[0] < 3:
    string_types = basestring
else:
    string_types = str


class Retrier(object):
    """重试器

    初始化参数说明：

        tries: 重试次数, 默认一直重试
        delay: 两次重试之间的间隔时间, 可以为一个 list 或 tuple 来指定每次间隔的时间,
            且重试次数为其长度
        exceptions: 指定哪些异常发生时需要重试, 可以是单个异常类型，
            也可以通过一个 list 或 tuple 指定多个异常类型
        exponential, max_delay: 时间间隔按指数增长, 如果 exponential 为 True,
            则每次的时间间隔为：(2 ** previous_tried_count) * delay
            max_delay 表示当增长到该值时不再增长，即超过该值后就永远为该值
        random_delay_min, random_delay_max: 随机间隔时间
        delay_func: 间隔函数, 如果指定则每次重试会调用该函数产生一个间隔时间,
            调用该函数时会把第几次重试作为参数传入
        logger: 日志记录器, 为 None 时不输出信息
        exception_log_level: 异常日志的输出级别，支持的级别为: info，warning，error

    间隔时间的类型优先级为: 函数间隔 > 列表间隔 > 指数间隔 > 随机间隔 > 固定间隔
    即如果指定了 exponential 为 True, 又同时指定了 random_delay_min, random_delay_max,
    则默认采用指数间隔
    """

    def __init__(self,
                 tries=None,
                 delay=None,
                 exceptions=Exception,
                 exponential=False, max_delay=None,
                 random_delay_min=None, random_delay_max=None,
                 delay_func=None,
                 logger=None,
                 exception_log_level="warning"):

        if exponential and not isinstance(delay, (int, float)):
            raise ValueError("when exponential is True, "
                             "delay must be an int or float type")

        self.tries = int(tries) if tries else -1

        self.delay = ((delay if isinstance(delay, (list, tuple))
                       else float(delay)) if delay else 0)

        self.exceptions = (tuple(exceptions) if isinstance(exceptions, list)
                           else exceptions)

        self.exponential = exponential

        self.max_delay = (float(max_delay) if max_delay is not None
                          else float("inf"))

        self.random_delay_min = random_delay_min
        self.random_delay_max = random_delay_max

        self.delay_func = delay_func

        if logger:
            self.log = logger
        else:
            logger_name = "{}.{}".format(__name__, self.__class__.__name__)
            self.log = logging.getLogger(logger_name)

        if exception_log_level not in ("info", "warning", "error"):
            raise ValueError("exception_log_level only supports info, warning, error")
        self.exception_log_level = exception_log_level

    @staticmethod
    def __add_suffix(num):
        num = str(num)
        suffix = {"1": "st", "2": "nd", "3": "rd"}
        suffix.update({n: "th" for n in list("0456789")})
        return num + suffix[num[-1]]

    def calc_delay(self, tried_times):
        if isinstance(self.delay, (list, tuple)):
            return float(self.delay[tried_times])
        elif self.exponential:
            delay = self.delay * (2 ** tried_times)
            return delay if delay < self.max_delay else self.max_delay
        elif self.random_delay_min or self.random_delay_max:
            random_delay_min = self.random_delay_min or 0
            random_delay_max = self.random_delay_max or 1 << 64
            return random.uniform(random_delay_min, random_delay_max)
        else:
            return self.delay

    def call(self, func, *args, **kwargs):
        log = self.log
        exceptions = self.exceptions

        trying_times = (len(self.delay) if isinstance(self.delay, (list, tuple))
                        else self.tries)

        exception_log = getattr(log, self.exception_log_level)

        trying_count = -1
        while 1:
            try:
                trying_count += 1
                return func(*args, **kwargs)
            except exceptions as e:
                if trying_times == 0:
                    raise

                exception_log("%s(args=%s, kwargs=%s) failed, trying_count=%s, "
                              "error message: %s",
                              func.__name__, args, kwargs, trying_count, e,
                              exc_info=True)

                trying_times -= 1
                delay = (self.delay_func(trying_count + 1) if self.delay_func
                         else self.calc_delay(trying_count))
                time.sleep(delay)

                log.info("will try to call '%s' for %s time",
                         func.__name__, self.__add_suffix(trying_count + 1))


if sys.version_info.major > 3:
    _wraps_safely = functools.wraps
else:
    def _wraps_safely(wrapper, *args, **kwargs):
        while isinstance(wrapper, functools.partial):
            wrapper = wrapper.func
        return functools.wraps(wrapper, *args, **kwargs)


def retry(*dargs, **dkw):
    if len(dargs) == 1 and callable(dargs[0]):

        def wrapper_simple(func):
            @_wraps_safely(func)
            def wrapped_func(*args, **kw):
                return Retrier().call(func, *args, **kw)
            return wrapped_func

        return wrapper_simple(dargs[0])

    else:

        def wrapper(func):
            @_wraps_safely(func)
            def wrapped_func(*args, **kw):
                return Retrier(*dargs, **dkw).call(func, *args, **kw)
            return wrapped_func

        return wrapper


class RetryingProxyObject(object):
    """重试代理对象

    对某对象进行代理访问，支持对象初始化重试，对象可调用方法重试
    """

    def __init__(self,
                 object_init_method,
                 object_init_params=None,
                 object_init_random=False,
                 object_init_retry_params=None,
                 object_attr_retry_params=None,
                 object_attr_on_exception=None,
                 logger=None):
        """初始化

        参数：
            object_init_method：对象初始化方法
            object_init_params：对象初始化参数，支持 dict 和 list 两种类型，
                为 dict 时，会以关键字参数的形式传给对象初始化方法；
                为 list 时，每个元素必须为 dict 类型，表示有多组初始化参数，
                用于在对象初始化失败时，尝试用另一个参数初始化，保证高可用性
            object_init_random：为 True 时，且 object_init_params 为一个 list 时，
                在初始化对象时会随机选择其中一个参数，否则按顺序依次尝试初始化
            object_init_retry_params：初始化对象时的失败重试参数
            object_attr_retry_params：对象的可调用属性执行失败时的重试次数
            object_attr_on_exception：对象的可调用属性执行重试仍然失败时被调用，
                可以为一个 string 类型或者可调用对象
                为 string 类型时表示获取对象方法来执行
            logger：日志记录器
        """
        if object_init_params is None:
            object_init_params = [{}]
        elif not isinstance(object_init_params, (tuple, list)):
            object_init_params = [object_init_params]
        for params in object_init_params:
            if not isinstance(params, dict):
                raise ValueError("Invalid object init params: '{}', "
                                 "must be a dict".format(params))

        if object_init_retry_params is None:
            object_init_retry_params = {
                "tries": 3,
                "delay": 1,
                "exponential": True,
            }

        object_init_method = retry(**object_init_retry_params)(
            object_init_method
        )

        if object_attr_retry_params is None:
            object_attr_retry_params = {
                "tries": 5,
                "delay": 1,
                "exponential": True,
            }

        if not logger:
            class_name = object.__getattribute__(self, "__class__").__name__
            logger_name = "{}.{}".format(__name__, class_name)
            logger = logging.getLogger(logger_name)

        setattribute = functools.partial(object.__setattr__, self)
        setattribute("__object_init_method__", object_init_method)
        setattribute("__object_init_params__", object_init_params)
        setattribute("__object_init_retry_params__", object_init_retry_params)
        setattribute("__object_attr_retry_params__", object_attr_retry_params)
        setattribute("__object_init_random__", object_init_random)
        setattribute("__object_attr_on_exception__", object_attr_on_exception)
        setattribute("__logger__", logger)

    @property
    def __obj__(self):
        getattribute = functools.partial(object.__getattribute__, self)
        try:
            obj = getattribute("__real_obj__")
        except AttributeError:
            object_init = getattribute("__object_init_method__")
            object_init_params = getattribute("__object_init_params__")
            if len(object_init_params) <= 1:
                try:
                    params = object_init_params[0]
                except IndexError:
                    params = {}
                obj = object_init(**params)
            else:
                object_init_params = copy(object_init_params)
                object_init_random = getattribute("__object_init_random__")
                logger = getattribute("__logger__")

                def gen_params():
                    if object_init_random:
                        length = len(object_init_params)
                        while length:
                            selected = random.randint(0, length - 1)
                            yield object_init_params.pop(selected)
                            length = len(object_init_params)
                    else:
                        for params in object_init_params:
                            yield params

                for params in gen_params():
                    logger.debug("Initializing object, selected params: %s",
                                 pprint.pformat(params))
                    try:
                        obj = object_init(**params)
                        break
                    except (IOError, OSError, Exception) as ex:
                        logger.exception("Initialize object error: %s, "
                                         "params: %s",
                                         ex, pprint.pformat(params))
                        continue
                else:
                    raise Exception("Initialize object failed")
            object.__setattr__(self, "__real_obj__", obj)
        return obj

    def __getattribute__(self, name):
        getattribute = functools.partial(object.__getattribute__, self)

        obj = getattribute("__obj__")
        if name == "__obj__":
            return obj

        attr = getattr(obj, name)

        if callable(attr):
            attr_retry_params = getattribute("__object_attr_retry_params__")
            attr_on_exception = getattribute("__object_attr_on_exception__")
            attr = retry(**attr_retry_params)(attr)
            if attr_on_exception:
                _attr = attr

                @functools.wraps(_attr)
                def wrapper(*args, **kwargs):
                    try:
                        ret = _attr(*args, **kwargs)
                    except Exception as ex:
                        if isinstance(attr_on_exception, string_types):
                            on_exception = getattr(obj, attr_on_exception)
                        else:
                            on_exception = attr_on_exception
                        logger = getattribute("__logger__")
                        logger.debug("Call '%s' error: %s, will be retried "
                                     "after call '%s'",
                                     _attr.__name__, ex, on_exception.__name__)
                        on_exception()
                        ret = _attr(*args, **kwargs)
                    return ret

                attr = wrapper

        return attr

    def __delattr__(self, name):
        delattr(object.__getattribute__(self, "__obj__"), name)

    def __setattr__(self, name, value):
        setattr(object.__getattribute__(self, "__obj__"), name, value)

    def __nonzero__(self):
        return bool(object.__getattribute__(self, "__obj__"))

    def __hash__(self):
        return hash(object.__getattribute__(self, "__obj__"))

    def __str__(self):
        return str(object.__getattribute__(self, "__obj__"))

    def __repr__(self):
        return repr(object.__getattribute__(self, "__obj__"))
